﻿/**
 * Peter
 * Created by: Peter Development Team
 *    http://peter.codeplex.com/
 * 
 * GNU General Public License version 2 (GPLv2)
 *    http://peter.codeplex.com/license
 *
 *  This code is provided on an AS IS basis, with no WARRANTIES,
 *  CONDITIONS or GUARANTEES of any kind.
 *  
 **/

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using Peter.Ftp.Data;

namespace Peter.Ftp.DataAccess
{
   /// <summary>
   /// Represents a FTP connection.
   /// http://www.dreamincode.net/forums/topic/35902-create-an-ftp-class-library-in-c%23/
   /// </summary>
   //*****************************************************************************************
   //                           LICENSE INFORMATION
   //*****************************************************************************************
   //   PCFtp Version 1.0.0.0
   //   Class library for FTP Client
   //
   //   Copyright (C) 2007  
   //   Richard L. McCutchen 
   //   Email: richard@psychocoder.net
   //   Created: 12JAN07
   //
   //   This program is free software: you can redistribute it and/or modify
   //   it under the terms of the GNU General Public License as published by
   //   the Free Software Foundation, either version 3 of the License, or
   //   (at your option) any later version.
   //
   //   This program is distributed in the hope that it will be useful,
   //   but WITHOUT ANY WARRANTY; without even the implied warranty of
   //   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   //   GNU General Public License for more details.
   //
   //   You should have received a copy of the GNU General Public License
   //   along with this program.  If not, see <http://www.gnu.org/licenses/>.
   //*****************************************************************************************
   public class FtpConnection : IDisposable
   {
      #region Fields

      private const int BUFFER_SIZE = 1024;
      /*private static readonly Regex s_ListRegex = new Regex (
         @"^(?<dir>[\-ld])(?<permission>([\-r][\-w][\-xs]){3})\s+(?<filecode>\d+)\s+(?<owner>\w+)\s+(?<group>\w+)\s+(?<size>\d+)\s+(?<timestamp>((?<month>\w{3})\s+(?<day>\d{1,2})\s+(?<hour>\d{1,2}):(?<minute>\d{2}))|((?<month>\w{3})\s+(?<day>\d{1,2})\s+(?<year>\d{4})))\s+(?<name>.+)$");*/

      private static readonly Encoding s_Ascii = Encoding.ASCII;
      private readonly Byte[] m_Buffer = new Byte[BUFFER_SIZE];
      private string m_WorkingPath = "/";
      private string m_StatusMessage = string.Empty;
      private string m_Result = string.Empty;
      private int m_Bytes;
      private bool m_IsBinary;
      private Socket m_FtpSocket;

      #endregion

      #region Class Properties

      /// <summary>
      /// Display all communications to the debug log
      /// </summary>
      public bool DoVerbose { get; set; }

      /// <summary>
      /// Gets the FTP connection's data.
      /// </summary>
      public FtpConnectionData ConnectionData { get; private set; }

      /// <summary>
      /// determine if the user is logged in
      /// </summary>
      public bool IsConnected { get; private set; }

      /// <summary>
      /// returns the status code of the command
      /// </summary>
      public int StatusCode { get; private set; }

      #endregion

      #region FTPFtpLogin
      /// <summary>
      /// method to log in to the remote ftp server
      /// </summary>
      /// <param name="connectionData">The connection data to use for transfers.</param>
      public async Task <bool> Connect (FtpConnectionData connectionData)
      {
         if (this.IsConnected) await this.CloseConnection ();
         // Ask for password...
         if (connectionData.AskForPassword)
         {
         }
         this.ConnectionData = connectionData;
         this.m_WorkingPath = this.ConnectionData.WorkingPath;
         WriteStatus ("Opening connection to " + this.ConnectionData.Server);
         if (await this.ConnectSocket ())
         {
            if (await this.ReadResponse (220))
            {
               await this.Execute ("USER " + this.ConnectionData.UserName);
               if (this.StatusCode == 331 || this.StatusCode == 230)
               {
                  if (this.StatusCode == 331)
                  {
                     await this.Execute ("PASS " + this.ConnectionData.Password);
                     if (!(this.StatusCode == 230 || this.StatusCode == 202))
                     {
                        this.LogOut ();
                        WriteError (this.ResultWithoutCode ());
                        return false;
                     }
                  }
                  this.IsConnected = true;
                  WriteStatus ("Connected to " + this.ConnectionData.Server);
                  await this.ChangeWorkingDirectory (this.m_WorkingPath);
                  return true;
               }
               LogOut ();
               WriteError (this.ResultWithoutCode ());
            }
            else
            {
               await this.CloseConnection ();
               WriteError (this.ResultWithoutCode ());
            }
         }
         return false;
      }

      /// <summary>
      /// Connects to the FTP socket.
      /// </summary>
      /// <returns>True if successful, otherwise false.</returns>
      private async Task <bool> ConnectSocket ()
      {
         try
         {
            this.m_FtpSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            var hostEntry = await Dns.GetHostEntryAsync (this.ConnectionData.Server);
            var remoteAddress = hostEntry.AddressList[0];
            var addrEndPoint = new IPEndPoint (remoteAddress, this.ConnectionData.Port);
            await Task.Factory.FromAsync (this.m_FtpSocket.BeginConnect,
               this.m_FtpSocket.EndConnect, addrEndPoint, null);
         }
         catch (Exception ex)
         {
            if (this.m_FtpSocket != null)
            {
               if (this.m_FtpSocket.Connected)
                  this.m_FtpSocket.Close ();
               this.m_FtpSocket.Dispose ();
               this.m_FtpSocket = null;
            }
            WriteError ("Couldn't connect to remote server.");
            WriteError (ex.Message);
            return false;
         }
         return true;
      }

      /// <summary>
      /// If the value of mode is true, set 
      /// binary mode for downloads, else, Ascii mode.
      /// </summary>
      private async Task <bool> SetBinary (bool isBinary)
      {
         if (this.m_IsBinary != isBinary)
         {
            this.m_IsBinary = isBinary;
            await this.Execute (this.m_IsBinary ? "TYPE I" : "TYPE A");
            if (this.StatusCode != 200)
            {
               WriteError (this.ResultWithoutCode ());
               return false;
            }
         }
         return true;
      }

      /// <summary>
      /// Gets the FTP result without the status code.
      /// </summary>
      /// <returns>FTP result without the status code.</returns>
      private string ResultWithoutCode ()
      {
         return this.m_Result.Substring (4);
      }

      /// <summary>
      /// Checks if we are currently connection, writing an error if we are not.
      /// </summary>
      /// <returns>True if connected, otherwise false.</returns>
      private bool CheckConnection ()
      {
         if (this.IsConnected) return true;
         Console.Error.WriteLine ("Peter.Ftp - You must be connected to perform this task.");
         return false;
      }

      /// <summary>
      /// Writes the given status to the standard output.
      /// </summary>
      /// <param name="status">Status to write.</param>
      private static void WriteStatus (string status)
      {
         Console.WriteLine ("Peter.Ftp|" + status);
      }

      /// <summary>
      /// Writes the given error to the error output.
      /// </summary>
      /// <param name="error">Error to write.</param>
      private static void WriteError (string error)
      {
         Console.Error.WriteLine ("Peter.Ftp|" + error);
      }
      #endregion

      #region CloseConnection

      /// <summary>
      /// Closes the connection.
      /// </summary>
      public async Task CloseConnection ()
      {
         if (this.ConnectionData != null)
         {
            WriteStatus ("Closing connection to " + this.ConnectionData.Server);
            if (this.m_FtpSocket != null)
               await this.Execute ("QUIT");
            this.LogOut ();
         }
      }
      #endregion

      #region ListFiles

      /// <summary>
      /// Return a string array containing the remote directory's file list.
      /// </summary>
      /// <param name="mask">The filter for the list command.</param>
      /// <param name="listCommand">The list command to send to the ftp server (LIST or NLST).</param>
      /// <returns>The list of files requested, null if any error.</returns>
      public async Task<IEnumerable<string>> ListFiles (string mask = "", string listCommand = "NLST")
      {
         string[] msg = null;
         //make sure the user is logged in
         if (this.CheckConnection ())
         {
            using (var dataSocket = await this.OpenSocketForTransfer ())
            {
               if (dataSocket == null) return null;
               dataSocket.ReceiveTimeout = this.ConnectionData.Timeout * 1000;
               await this.Execute (listCommand + " " + mask);
               if (!(this.StatusCode == 150 || this.StatusCode == 125))
               {
                  WriteError (this.ResultWithoutCode ());
                  return null;
               }

               this.m_StatusMessage = "";
               while (true)
               {
                  //retrieve the data from the host
                  int bytes = dataSocket.Receive (this.m_Buffer, this.m_Buffer.Length, 0);
                  //convert it to Ascii format
                  this.m_StatusMessage += s_Ascii.GetString (this.m_Buffer, 0, bytes);
                  //exit the method is nothing is returned
                  if (bytes < this.m_Buffer.Length) break;
               }
               msg = this.m_StatusMessage.Replace ("\r", "")
                  .Split (new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
            }
            //check the return message
            if (this.m_StatusMessage.IndexOf ("No such file or directory",
               StringComparison.Ordinal) != -1)
            {
               WriteError ("No such file or directory");
               msg = null;
            }
            //read the host's response
            await this.ReadResponse ();
            if (this.StatusCode != 226)
            {
               WriteError (this.m_StatusMessage.Substring (4));
               msg = null;
            }
            if (msg != null)
            {
               Array.Sort (msg);
            }
         }

         return msg;
      }

      #endregion

      #region GetFileSize

      /// <summary>
      /// Method to retrieve the size of the file based
      /// on the name provided
      /// </summary>
      /// <param name="file">Name of the file to get the size of</param>
      /// <param name="quite">If true, error messages with be suppressed.</param>
      /// <returns>The files size</returns>
      public async Task <long> GetFileSize (string file, bool quite = false)
      {
         long size = -1;
         if (this.CheckConnection ())
         {
            await this.Execute ("SIZE " + file);
            if (this.StatusCode == 213)
               size = long.Parse (this.ResultWithoutCode ());
            else if (!quite)
               WriteError (this.ResultWithoutCode ());
         }
         return size;
      }

      #endregion

      #region DownloadFile
      /// <summary>
      /// Download a remote file to a local file name which can include
      /// a path, and set the resume flag. The local file name will be
      /// created or overwritten, but the path must exist.
      /// </summary>
      /// <param name="ftpFile">File on the server to download</param>
      /// <param name="localFile">Name of the file on the local machine</param>
      /// <param name="resume"></param>
      public async Task <bool> DownloadFile (string ftpFile, string localFile, Boolean resume)
      {
         //make sure the user is logged in
         if (this.CheckConnection ())
         {
            if (!(await this.SetBinary (true)))
            {
               WriteError ("Unable to change to binary mode.");
               return false;
            }
            //display a downloading file message
            WriteStatus ("Downloading '" + ftpFile + "' from " + this.ConnectionData.Server + "/" +
                               this.m_WorkingPath);
            //check if a local file name was provided
            //if not then set its value to the ftp file name
            if (localFile.Equals (""))
               localFile = ftpFile;
            var output = !File.Exists (localFile)
               ? File.Create (localFile)
               : new FileStream (localFile, FileMode.Open);

            using (var dataSocket = await OpenSocketForTransfer ())
            {
               dataSocket.ReceiveTimeout = this.ConnectionData.Timeout * 1000;
               //create our resume point
               //if resume was set to true
               if (resume)
               {
                  //set the value of our resume variable
                  long resumeOffset = output.Length;
                  //check if its value is greater than 0 (zero)
                  if (resumeOffset > 0)
                  {
                     //execute our rest command, which sets the
                     //resume point for the download in case
                     //the download is interrupted
                     await this.Execute ("REST " + resumeOffset);
                     //check the status code, if not a 350
                     //code then the server doesnt support resuming
                     if (StatusCode != 350)
                        WriteError ("Resuming not supported: " + this.ResultWithoutCode ());
                     else
                     {
                        WriteStatus ("Resuming at offset " + resumeOffset);
                        output.Seek (resumeOffset, SeekOrigin.Begin);
                     }
                  }
               }
               //execute out retr command
               //which starts the file transfer
               await this.Execute ("RETR " + ftpFile);
               //check the status code, we need 150 or 125
               //otherwise the download failed
               if (StatusCode != 150 && StatusCode != 125)
               {
                  WriteError (this.ResultWithoutCode ());
                  await this.SetBinary (false);
                  return false;
               }
               while (true)
               {
                  //receive the binary data from the socket
                  m_Bytes = dataSocket.Receive (m_Buffer, m_Buffer.Length, 0);
                  //write the file
                  output.Write (m_Buffer, 0, m_Bytes);
                  //make sure the file is greater than
                  //zero in size, if not exit the method
                  if (m_Bytes <= 0)
                     break;
               }
               //close our stream
               output.Close ();
            }
            //read the host's response
            await ReadResponse ();
            //we're looking for a status code of 226 or 250,
            //if that isnt returned the download failed
            if (StatusCode != 226 && StatusCode != 250)
            {
               WriteError (this.ResultWithoutCode ());
               await this.SetBinary (false);
               return false;
            }
            await this.SetBinary (false);
            return true;
         }
         return false;
      }
      #endregion

      #region UploadFile
      /// <summary>
      /// Upload a file and set the resume flag.
      /// </summary>
      /// <param name="fileName">Full Path of file to upload.</param>
      /// <param name="resume">True if we are resuming an upload.</param>
      public async Task <bool> UploadFile (string fileName, bool resume)
      {
         if (!File.Exists (fileName))
         {
            WriteError (string.Format("The file '{0}' does not exist locally.", fileName));
            return false;
         }

         //make sure the user is logged in
         if (this.CheckConnection ())
         {
            long resumeOffset = 0;
            if (resume)
            {
               if (!(await this.SetBinary (true)))
               {
                  WriteError ("Unable to change to binary mode.");
                  return false;
               }
               resumeOffset = await GetFileSize (Path.GetFileName (fileName));
               if (resumeOffset == -1) resumeOffset = 0;
            }

            // open stream to read file
            var input = new FileStream (fileName, FileMode.Open);
            //if resume is true
            //and the size of the file read is
            //less than the initial value
            if (resume && input.Length < resumeOffset)
            {
               // different file size
               WriteStatus ("Overwriting " + fileName);
               resumeOffset = 0;
            }
            else if (resume && input.Length == resumeOffset)
            {
               // file done
               input.Close ();
               WriteStatus ("Skipping completed " + fileName + " - turn resume off to not detect.");
               return true;
            }

            //now create our socket needed for
            //the file transfer
            Socket dataSocket = await OpenSocketForTransfer ();
            //if the file size is greater than 0
            if (resumeOffset > 0)
            {
               //execute the rest command, which
               //sets the point the resume will occurr
               //if the upload is interrupted
               await this.Execute ("REST " + resumeOffset);
               //check the status code, if it's not
               //350 the resume isnt supported by the server
               if (StatusCode != 350)
               {
                  Console.Error.WriteLine ("Resuming not supported");
                  resumeOffset = 0;
               }
            }
            //execute the store ftp command (starts the transfer of the file)
            await this.Execute ("STOR " + Path.GetFileName (fileName));
            //check the status code, we need a
            //value of 150 or 125, otherwise throw an exception
            if (StatusCode != 125 && StatusCode != 150)
            {
               WriteError (this.ResultWithoutCode ());
               await this.SetBinary (false);
               return false;
            }
            //now check the resumeOffset value,
            //if its not zero then we need to resume
            //the upload process where it ended
            if (resumeOffset != 0)
            {
               //let the user know the upload is resuming
               WriteStatus ("Resuming at offset " + resumeOffset);
               //use the Seek method to get to where the upload ended
               input.Seek (resumeOffset, SeekOrigin.Begin);
            }
            //let the user know the uploading has begun
            WriteStatus ("Uploading file " + fileName + " to " + m_WorkingPath);
            //upload the file
            while ((m_Bytes = input.Read (m_Buffer, 0, m_Buffer.Length)) > 0)
            {
               dataSocket.Send (m_Buffer, m_Bytes, 0);
            }
            //close our reader
            input.Close ();
            //check to see if the socket is still connected
            //if it is then disconnect it
            if (dataSocket.Connected)
            {
               dataSocket.Close ();
            }
            //read the host's response
            await ReadResponse ();
            //checking for a successful upload code (226 or 250)
            //if not either then throw an exception
            if (StatusCode != 226 && StatusCode != 250)
            {
               WriteError (m_Result.Substring (4));
               await this.SetBinary (false);
               return false;
            }
            await this.SetBinary (false);
            return true;
         }
         return false;
      }
      #endregion

      #region UploadDirectory
      /// <summary>
      /// Upload a directory and its file contents
      /// </summary>
      /// <param name="dirPath">Path of the directory to upload</param>
      /// <param name="recursive">Whether to recurse sub directories</param>
      /// <param name="fileMask">Only upload files of the given mask(i.e;'*.*','*.jpg', ect..)</param>
      public async void UploadDirectory (string dirPath, bool recursive, string fileMask)
      {
         //make sure the user is logged in
         if (this.CheckConnection ())
         {
            string [] directories = dirPath.Replace ("/", @"\").Split ('\\');
            string rootDirectory = directories [directories.Length - 1];

            // make the root dir if it does not exist
            var files = await ListFiles (rootDirectory);
            if (!files.Any())
            {
               await CreateDirectory (rootDirectory);
            }
            //make the new directory the working directory
            await ChangeWorkingDirectory (rootDirectory);
            //loop through the files in the directory
            foreach (string file in Directory.GetFiles (dirPath, fileMask))
            {
               //upload each file
               await UploadFile (file, true);
            }
            //check if recusrsive was specified
            if (recursive)
            {
               //since recursive is true, we loop through all the
               //directories in the directory provided
               foreach (string directory in Directory.GetDirectories (dirPath))
               {
                  //upload each directory
                  UploadDirectory (directory, true, fileMask);
               }
            }
            //change working directory back to root level
            await ChangeWorkingDirectory ("..");
         }
      }
      #endregion

      #region DeleteFile
      /// <summary>
      /// method to delete a file from the FTP server.
      /// </summary>
      /// <param name="file">File to delete</param>
      public async Task <bool> DeleteFile (string file)
      {
         var ok = false;
         if (this.CheckConnection ())
         {
            await this.Execute ("DELE " + file);
            if (this.StatusCode == 250)
            {
               ok = true;
               WriteStatus ("Deleted file " + file);
            }
            else
               WriteError (this.ResultWithoutCode ());
         }
         return ok;
      }
      #endregion

      #region RenameFile
      /// <summary>
      /// Rename a file on the remote FTP server.
      /// </summary>
      /// <param name="oldName">File to rename</param>
      /// <param name="newName">New name of the file</param>
      /// <param name="replace">setting to false will throw exception if it exists</param>
      public async Task <bool> RenameFile (string oldName, string newName, bool replace)
      {
         if (oldName == newName)
         {
            WriteError ("You cannot rename a file with the same name.");
            return false;
         }
         //make sure the user is logged in
         if (this.CheckConnection ())
         {
            //execute the rename from command
            await this.Execute ("RNFR " + oldName);
            //check for a status code of 350
            if (StatusCode != 350)
            {
               WriteError (this.ResultWithoutCode ());
               return false;
            }
            //if they didnt choose to replace the file, and a 
            //file with that name already exists then throw an exception
            var files = await ListFiles (newName);
            if (!replace && files.Any())
            {
               WriteError ("File already exists");
               return false;
            }
            //execute the rename to command
            await this.Execute ("RNTO " + newName);
            //check for a status code of 250, if
            //not then throw an exception
            if (StatusCode != 250)
            {
               WriteError (this.ResultWithoutCode ());
               return false;
            }
            //write the successful message out
            WriteStatus ("Renamed file " + oldName + " to " + newName);
            return true;
         }
         return false;
      }
      #endregion

      #region CreateDirectory
      /// <summary>
      /// Create a directory on the remote FTP server.
      /// </summary>
      /// <param name="dirName">Name of the directory to create</param>
      public async Task <bool> CreateDirectory (string dirName)
      {
         //check to make sure a directory name was supplied
         if (string.IsNullOrEmpty (dirName) || dirName.Equals (".")) return false;

         //make sure the user is logged in
         if (this.CheckConnection ())
         {
            //execute the make directory command
            await this.Execute ("MKD " + dirName);
            //check for a status code of 250 or 257
            if (StatusCode != 250 && StatusCode != 257)
            {
               //operation failed, throw an exception
               WriteError (this.ResultWithoutCode ());
               return false;
            }

            WriteStatus ("Created directory " + dirName);
            return true;
         }
         return false;
      }
      #endregion

      #region RemoveDirectory
      /// <summary>
      /// Delete a directory on the remote FTP server.
      /// </summary>
      /// <param name="dirName"></param>
      public async Task <bool> RemoveDirectory (string dirName)
      {
         //check to make sure a directory name was supplied
         if (string.IsNullOrEmpty (dirName) || dirName.Equals (".")) return false;

         //make sure the user is logged in
         if (this.CheckConnection ())
         {
            //execute the remove directory command
            await this.Execute ("RMD " + dirName);
            //check for a status code of 250
            if (StatusCode != 250)
            {
               WriteError (this.ResultWithoutCode ());
               return false;
            }
            //we made it this far so print the name
            //of the removed directory to the window
            WriteStatus ("Removed directory " + dirName);
            return true;
         }
         return false;
      }
      #endregion

      #region ChangeWorkingDirectory
      /// <summary>
      /// Change the current working directory on the remote FTP server.
      /// </summary>
      /// <param name="dirName"></param>
      public async Task <bool> ChangeWorkingDirectory (string dirName)
      {
         //check to make sure a directory name was supplied
         if (string.IsNullOrEmpty (dirName) || dirName.Equals (".")) return false;

         if (this.CheckConnection ())
         {
            //execute the CWD command = Change Working Directory
            await this.Execute ("CWD " + dirName);
            if (this.StatusCode == 250)
            {
               //Print Working Directory
               await this.Execute ("PWD");
               if (this.StatusCode == 257)
               {
                  this.m_WorkingPath = this.m_StatusMessage.Split ('"')[1];
                  WriteStatus ("Current directory is " + this.m_WorkingPath);
                  return true;
               }
               WriteError (this.ResultWithoutCode ());
            }
            else
               WriteError (this.ResultWithoutCode ());
         }
         return false;
      }
      #endregion

      #region readResponse

      /// <summary>
      /// Reads the response and verifies that the status code is as expected.
      /// </summary>
      /// <param name="expectedCode">Expected status codes.</param>
      /// <returns>True if expected status code was returned, otherwise false.</returns>
      private async Task <bool> ReadResponse (int expectedCode)
      {
         return await this.ReadResponse () == expectedCode;
      }

      /// <summary>
      /// Reads the host response.
      /// </summary>
      private async Task <int> ReadResponse ()
      {
         m_StatusMessage = "";
         m_Result = await ParseHostResponse ();
         StatusCode = int.Parse (m_Result.Substring (0, 3));
         return StatusCode;
      }
      #endregion

      #region ParseHostResponse

      /// <summary>
      /// Method to parse the response from the remote host
      /// </summary>
      /// <returns></returns>
      private async Task <string> ParseHostResponse ()
      {
         while (true)
         {
            while (true)
            {
               //retrieve the host response and convert it to
               //a byte array
               //m_Bytes = m_FtpSocket.Receive (m_Buffer, m_Buffer.Length, 0);
               var result = this.m_FtpSocket.BeginReceive (this.m_Buffer, 0, this.m_Buffer.Length, SocketFlags.None, null, null);
               if (result != null)
                  m_Bytes = await Task <int>.Factory.FromAsync (result, this.m_FtpSocket.EndReceive);
               //decode the byte array and set the
               //statusMessage to its value
               m_StatusMessage += s_Ascii.GetString (m_Buffer, 0, m_Bytes);
               //check the size of the byte array
               if (m_Bytes < m_Buffer.Length)
               {
                  break;
               }
            }
            //split the host response
            string [] msg = m_StatusMessage.Split ('\n');
            //check the length of the response
            m_StatusMessage = m_StatusMessage.Length > 2 ? msg [msg.Length - 2] : msg [0];

            //check for a space in the host response, if it exists return
            //the message to the client
            if (!m_StatusMessage.Substring (3, 1).Equals (" ")) continue;
            //check if the user selected verbose Debugging
            if (this.DoVerbose)
            {
               //loop through the message from the host
               for (int i = 0; i < msg.Length - 1; i++)
               {
                  //write each line out to the window
                  Console.Write (msg [i]);
               }
            }
            //return the message
            return m_StatusMessage;
         }
      }

      #endregion

      #region Execute
      /// <summary>
      /// method to send the ftp commands to the remove server
      /// </summary>
      /// <param name="command">the command to execute</param>
      private async Task <int> Execute (String command)
      {
         //check to see if verbose debugging is enabled
         //if so write the command to the window
         if (this.DoVerbose) Console.WriteLine (command);
         //convert the command to a byte array
         Byte[] cmdBytes = Encoding.ASCII.GetBytes ((command + "\r\n").ToCharArray ());
         //send the command to the host
         var result = this.m_FtpSocket.BeginSend (cmdBytes, 0, cmdBytes.Length, SocketFlags.None, null, null);
         if (result != null)
            await Task<int>.Factory.FromAsync (result, this.m_FtpSocket.EndSend);
         //read the returned response
         return await this.ReadResponse ();
      }
      #endregion

      #region OpenSocketForTransfer
      /// <summary>
      /// when doing data transfers, we need to open another socket for it.
      /// </summary>
      /// <returns>Connected socket</returns>
      private async Task <Socket> OpenSocketForTransfer ()
      {
         //send the PASV command (Passive command)
         await this.Execute ("PASV");
         //check the status code, if it
         //isnt 227 (successful) then throw an exception
         if (StatusCode != 227)
         {
            WriteError (this.ResultWithoutCode ());
            return null;
         }
         //find the index of the opening "("
         //and the closing ")". The return
         //message from the server, if successful, has
         //the IP and port number for the client in
         //enclosed in "(" & ")"
         int idx1 = m_Result.IndexOf ('(');
         int idx2 = m_Result.IndexOf (')');
         //now we need to get everything in the parenthesis
         string ipData = m_Result.Substring ((idx1 + 1), (idx2 - idx1) - 1);
         //create new integer array with size of 6
         //the returning message is in 6 segments
         var msgSegments = new int[6];
         //get the length of the message
         int msgLength = ipData.Length;
         int partCount = 0;
         string buffer = "";
         //now we need to loop through the host response
         for (int i = 0; i < msgLength && partCount <= 6; i++)
         {
            //convert each character to a char
            char chr = char.Parse (ipData.Substring (i, 1));
            //check to see if the current character is numeric
            if (char.IsDigit (chr))
            {
               //since its a number we add it to our buffer variable
               buffer += chr;
            }
            //now we need to check for the
            //comma seperating the digits
            else if (chr != ',')
            {
               //no comma so throw an exception
               WriteError ("Malformed PASV result: " + m_Result);
               return null;
            }
            //else
            //{
            //check to see if the current character is a comma
            //or if the counter + 1 equals the host response length
            if (chr == ',' || i + 1 == msgLength)
            {
               try
               {
                  //since its one of the 2 we add it to the
                  //current index of the message segments
                  msgSegments[partCount++] = int.Parse (buffer);
                  buffer = "";
               }
               //handle any exceptions thrown
               catch (Exception ex)
               {
                  WriteError ("Malformed PASV result (not supported?): " + m_Result);
                  WriteError (ex.Message);
                  return null;
               }
            }
            //}
         }
         //now we assemble the IP address returned from the host
         string ipAddress = msgSegments[0] + "." + msgSegments[1] + "." + msgSegments[2] + "." + msgSegments[3];
         //the last 2 segments are the port we need to use
         //int port = (msgSegments[4] << 8) + msgSegments[5];
         int port = (msgSegments[4] * 256) + msgSegments[5];

         Socket tranferSocket = null;

         try
         {
            //create our new socket for transfering data
            tranferSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            var ipEndPoint = new IPEndPoint (Dns.GetHostEntry (this.ConnectionData.Server).AddressList[0], port);
            tranferSocket.Connect (ipEndPoint);
         }
         catch (Exception ex)
         {
            // doubtfull....
            if (tranferSocket != null && tranferSocket.Connected) tranferSocket.Close ();
            //throw an FtpException
            Console.Error.WriteLine ("Can't connect to remote server!");
            Console.Error.WriteLine (ex.Message);
            tranferSocket = null;
         }
         //return the socket
         return tranferSocket;
      }

      #endregion

      #region LogOut
      /// <summary>
      /// method to release and remove any sockets left open
      /// </summary>
      private void LogOut ()
      {
         //check to see if the sock is non existant
         if (this.m_FtpSocket != null)
         {
            this.m_FtpSocket.Dispose ();
            this.m_FtpSocket = null;
         }
         //log the user out
         this.IsConnected = false;
      }
      #endregion

      #region Destructor
      /// <summary>
      /// Destuctor
      /// </summary>
      ~FtpConnection ()
      {
         LogOut ();
      }

      /// <summary>
      /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
      /// </summary>
      /// <filterpriority>2</filterpriority>
      public void Dispose ()
      {
         Dispose (true);
         GC.SuppressFinalize (this);
      }

      /// <summary>
      /// Disposes the object.
      /// </summary>
      /// <param name="disposing">True to release managed resources.</param>
      private async void Dispose (bool disposing)
      {
         if (disposing)
         {
            await this.CloseConnection ();
         }
      }
      #endregion
   }
}
